=====================
Winsock
API Tutorial Part 4
Written
By
Mohamed
Fayed
=====================
برمجة الخادم ((Server
Binding:
في الدرسين السابقين تعرفنا علي
كيفية استدعاء خدمة أل Winsock وإنشاء مقبس ومعرفة قيمة مقبضه.
الآن سنتعرف على كيفية استخدام هذا المقبس في ربطه على منفذ Port معين
ووضع المنفذ في حالة استماع – تماما مثل أداه MsWinsck.ocx-
وانتظار قدوم أي إشعار بالاتصال من الطرف الأخر
ولكي نقوم بربط المقبس
على عنوان IP و منفذ Prot نستخدم الدالة bind()
وتجد التصريح لها هكذا
Public Declare Function bind Lib "ws2_32.dll" ( _
ByVal s As Long, ByRef Name As sockaddr_in, ByRef namelen As Long) As Long
حيث تمثل الثلاث توابع الأتى :
S
قيمة المقبض
الناتج
عن استخدام دالة Socket()
sockaddr_in بنية يتم ملؤها بمعلومات مثل المنفذ
المستخدم والعنوان IP
namelen حجم البنية sockaddr_in .
و أل Syntax لدالة bind() :
lRetVal =
bind(LlistenSocket, sockaddr_in,
Len(sockaddr_in))
لو لاحظت فى تصريحات mdlWinsock
Public Type sockaddr_in
sin_family As Integer
sin_port As Integer
sin_addr As Long
End Type
هذه البنية sockaddr_in تتكون من :
sin_family : تمثل عائلة
العنوان ونضع بها القيمة AF_INET لأننا نستخدم العائلة Ipv4.
:
sin_port رقم المنفذ
المراد الاستماع عليه.
:
sin_addr تحديد عنوان IP للطرف الآخر، واستخدمنا هنا العنوان INADDR_ANY فهو يجعل
الخادم يقبل أي
اتصال من أي عنوان IP..فهو ليس محدد بقبول عنوان
معين.
Public Const INADDR_ANY = &H0 'From mdlWinsock
Declrationss
Public Const
AF_INET = 2 'UDP, TCP,..
Public Const
IPPROTO_TCP = 6 'TCP Protocol
Public Const
SOCK_STREAM = 1
ويتم ملء بنية sockaddr_in هكذا:
sockaddr_in.sin_family = AF_INET 'IP Protocol Family
sockaddr_in.sin_addr = INADDR_ANY 'accecpt any
incoming connection requests
sockaddr_in.sin_port = htons(CLng(5000(( 'on port 5000
ولاحظ أننا استخدمنا دالة htons - أو ((Host byte To NetWork byte order For Short Numbers وأل
Short Numbers
ستحمل قيم نوعها Integer وهي أرقام المنافذ من صفر إلى 65535 -ويمكنك مراجعة التصريح الذي وضعناه للدالة - .وذلك لاختلاف
الشبكات فتم الاتفاق على طريقة واحدة لتمثيل العناوين .فأي
عنوان IP أو Port يمرر عبر مقابس الويندوز في بنية sockaddr_in - ما عدا sin_family طبعا
- لازم أن يكون من نوع الشبكة((Must be In
Network Byte Order.
فأي عنوان IP وليكن 127.0.0.1 لابد من أن يكون ممثل بالبايت وهذا ما تقوم الدالة بعمله .
طبعا سنستخدم دوال تساعدنا علي هذا التمثيل مثل htons() لأرقام المنافذ ولها دالة
للتحويل العكسي - للعلم فقط- تدعى ntohs() .وسنستخدم عندما نتصل بالخادم دالة inet_addr() لتحويل رقم IP من عنوان مفصول بـ (.) إلى عنوان 32 bit من نوع الشبكة.
ويبقى أن نربط معلومات بنية sockaddr_in بالمقبس الذى
انشئناه وذلك عن طريق دالة bind
lRetVal = bind(LlistenSocket, sockaddr_in, Len(sockaddr_in))
إذا حدث خطأ فسيعود المتغير lRetVal ب
SOCKET_ERROR وأيضا
قيمتها ((-1 .
Public Const SOCKET_ERROR = -1 'From mdlWinsock Declrations
___________________________________________________________________________
هناك خطأ شائع وهو WSAEADDRINUSE وقيمتها 10048وهو يفيد بأن المنفذ Port في حالة استخدام من قبل برنامج أخر
.
ولكي تتغلب على هذه
المشكلة ببساطة , قم بتغيير رقم المنفذ.
Public
Const WSAEADDRINUSE = 10048
الآن نستطيع أن نضع الخادم في حالة استماع بواسطة الدالة Listen()
والتصريح لها هو
Public Declare Function listen Lib
"ws2_32.dll"
(ByVal s As Long, ByVal backlog As Long) As Long
ويوجد
معاملين للدالة هما:
S : وهو مقبض المقبس.
Backlog :وهو متغير رقمي يخزن به أقصى عدد لطلبات الاتصال في صف انتظار الخادم.
وال Syntax للدالة هو :
lRetVal = listen(LlistenSocket, 1)
Accepting Connection Requests :
بعدما وضعنا الخادم في حالة استماع لأي اتصال قادم يبقى أن نوافق على طلب الاتصال
وذلك باستخدام الدالة Accept()
Public Declare Function accept Lib "ws2_32.dll" (ByVal s As Long, addr As sockaddr_in, namelen As Long) As Long
الآن
سنكتب تطبيقServer
يبين كيفية إنشاء المقبس وكيفية ربطه مع منفذ وكيفية وضعه في حالة استماع و
الموافقة على أي اتصال قادم وكيفية إرسال واستقبال بيانات .
سيتكون المشروع من:
1- فورم تدعى frmserver
2- وحدة نمطية تدعى mdlWinsock وسنضع بها
تعريفات مكتبة الوينسوك زائد كود إلقاء خطاف رسائل المقبس Hook
3- وحدة نمطية أخري أخرى
للتصنيف الفرعي تدعى mdlSubClass .
وهذا هو ملخص ما سنفعله
:
أولا :
في حدث أل Form_Load لكلا من ألClient و
أل Server سنلقى بخطاف Hook وذلك لمراقبة
رسائل المقبس
( Socket).
ثانيا :
سننشأ إجراءان فرعيان لكلا من (تهيئة وتحميل مكتبة الوينسوك) وسيكون باسم
WinsockInitialize() فى الوحدة
النمطية mdlWinsock
وأجراء أخر لإنشاء المقبس
وعمل bind له ثم وضع المقبس فى حالة استماع
.وليكن الأجراء
باسم WinsockListen()
ثالثا :
الخطاف hook الذي وضعناه سيكون عنوان تصنيفه الفرعي SubClass هو WinsockProc()
وفى هذا التصنيف الفرعي.
سنتعرف على الرسائل التي يستقبلها المقبس .وكيفية معالجة كل رسالة والرد عليها
.
سيتكون مشروعنا من الأتى
|
الأداة |
الأسم |
خواص |
|
|
|
CmdSend |
_ |
|
|
|
txtChat |
MultiLine=True |
|
|
|
txtSend |
_ |
|
|
|
lblstatus |
_ |
|
ويكون شكل المشروع كالتالى

نبدأ بكتابة كود FrmServer :
Private
Sub Form_Load()
HookForm frmServer.hwnd 'إلقاء خطاف للرسائل
If WinsockInitialize
= True Then
' قمنا بتحميل المكتبة عن طريق إجراء فرعى سنكتبه وحدة نمطية
WinsockListen
' وبأجراء فرعى أخر قمنا بإنشاء مقبس وربطه ووضعه فى حالة استماع
End If
End
Sub
______________________
Private
Sub Form_Unload(Cancel As Integer)
UnHookForm frmServer.hwnd 'إزالة الخطاف
If LlistenSocket <> INVALID_SOCKET Then closesocket LlistenSocket
' الكود السابق خاص بغلق المقبس اذا
كان مفتوح
WSACleanup ' ثم إلغاء
تحميل المكتبة
End
Sub
'______________________
Private
Sub cmdSend_Click()
Dim
Message As String
If Len(txtSend.Text) = 0 Then Exit
Sub
Message
= "Reply" & "Server : " & txtSend.Text & vbCrLf
lRetVal = send(LlistenClient,
ByVal Message, Len(Message), 0)
If lRetVal = SOCKET_ERROR Then
lblStatus.Caption
= "You Are Not Connected"
Exit Sub
End
If
txtChat.Text = txtChat & "Server : " & txtSend.Text
& vbCrLf
txtSend.Text = ""
End Sub
في كود ارسال الرسالة إلى العميل Client استخدمنا الدالة Send()
Declare Function send Lib "ws2_32.dll" (ByVal s As Long, buf As Any, _
ByVal buflen As Long,ByVal flags As Long) As Long
والدالة لها أربع وسيطات كما ترى :
S : القيمة
المعادة عند قبول الاتصال والتي تحدد لنا الطرف الأخر.
Buf :مخزن data للبيانات - (بالبايت) - والتي سترسل إلى الطرف الأخر.
Buflen :حجم البيانات .
Flags :تابع اختياري ولن نستخدمه وسنضع القيمة
له بصفر ..وهو يتحكم في طريقة نقل البيانات.
وكود الإرسال كان هكذا
:
send(LlistenClient, ByVal data, Len(data), 0)
ولتوضيح ما هو LlistenClient ينبغي أن نلقى نظرة على
دالة Accept() والتي استقبلنا بها
الاتصال القادم من الطرف الأخر
LlistenClient = accept(wParam, ClientInternetAddress,
ByVal AddressLen)
فعندما يتم قبول الاتصال تعود دالة Accept() بقيمة رقمية نخزنها فى متغير من نوع
Long هو LlistenClient ,تلك القيمة
نستطيع منها تحديد الجهاز المتصل مع الخادم Server, والذي يتم
إرسال البيانات إلي الخادم منه, وبالتالي عند تصميم برنامج MultiClients
مثل برنامج Net Café
Controller يمكننا تحديد
كل جهاز متصل مع الخادم Server
عن طريق تلك القيمة, ونستطيع إرسال أي رسالة إلي أي جهاز متصل أيضا عن طريق تلك
القيمة.
ولازم عندما نستخدم دالة Send() أن نحدد له مقبض (handle)
الطرف الأخر الذى نتعامل معه وسيلي شرح دالة Accept
.
إذا لم يحدث أي خطأ أثناء إرسال
البيانات فتعود الدالة بعدد البايتات ((Bytes التي أرسلت
وإذا حدث خطأ فتعود ب SOCKET_ERROR وقيمتها (-1) .
هذا هو كل ما سنكتبه في فورم FrmServer وننطلق الآن إلى كتابة اكواد
الوحدة النمطية mdlWinsock
سنستخدم طبعا تصريحات ألWinsock API ونضيف عليها التصريحات الآتية والخاصة بعمل hook وSubclass:
Public lpWinsock as long
Public
Const GWL_WNDPROC = (-4)
Public
Declare Function SetWindowLong Lib "user32"
Alias "SetWindowLongA" (ByVal
_
Hwnd As Long,ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Declare
Function CallWindowProc Lib "user32" Alias
"CallWindowProcA" (ByVal
lpPrevWndFunc As Long, ByVal
& _
hwnd
As Long, ByVal MSG As Long, ByVal
wParam As Long, ByVal lParam As Long) As Long
وستكون الاكواد
المضافة على الوحدة النمطية خاصة بإجراءات فرعية لتحميل مكتبة الوينسوك
وإنشاء مقبس ووضعه في حالة استماع .وعمل
الإجراءات الفرعية لخطاف الرسائل Hook و Unhook
Public
Function WinsockInitialize() As Boolean
Dim
lRetVal As Long
Dim
WSAData As WSADataType
lRetVal = WSAStartup(&H202,
WSAData)
If lRetVal <> 0 Then
frmServer.lblStatus
= "Can't Load winsock Service"
WinsockCleanup
Exit Function
End
If
WinsockInitialize = True
' هذه القيمة السابقة لن تظهر إلا لو نجحت الدالة في
تحميل المكتبة وبالتالي يمكننا
' Form_Load استخدامها كشرط لإنشاء مقبس كما فعلنا
في حدث
End
Function
Public
Sub HookForm(hwnd As Long)
lpWinsock = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf Winsockmain)
' Winsockmain تثبيت الخطاف لتصنيف فرعى
End
Sub
Public
Sub UnHookForm(hwnd As Long)
SetWindowLong hwnd, GWL_WNDPROC, lpWinsock 'ازالة الخطاف
End
Sub
Public
Sub WinsockListen()
Dim
InternetAdrr As sockaddr_in
On
Error Resume Next
LlistenSocket = socket(AF_INET,
SOCK_STREAM, IPPROTO_TCP)
If listenSocket = INVALID_SOCKET Then
frmServer.lblStatus.Caption
= "Error While Creating Socket"
Exit Sub
End
If
InternetAdrr.sin_family = AF_INET
InternetAdrr.sin_port = htons(CLng("2010"))
InternetAdrr.sin_addr = INADDR_ANY
lRetVal = bind(Winsock, InternetAdrr, Len(InternetAdrr))
If lRetVal = SOCKET_ERROR Then
closesocket
LlistenSocet
frmServer.lblStatus.Caption
= "Error While binding Socket"
Exit Sub
End
If
WSAAsyncSelect LlistenSocket, frmServer.hwnd, WM_SOCKET, FD_ACCEPT
'سيلى شرح
الدالة السابقة بالتفصيل
lRetVal = listen(LlistenSocket,
1)
If lRetVal = SOCKET_ERROR Then
closesocket
LlistenSocket
frmServer.lblStatus.Caption
= "Error While Listening on port 2010"
Exit Sub
End
If
frmServer.lblStatus.Caption = "Ready To
Accept Any Request"
End Sub
الدالة WSAAsyncSelect تمكننا
من استقبال أحداث المقبس Socket Events من خلال
الرسائل الواردة إليه:
Declare
Function WSAAsyncSelect Lib "wsock32.dll" (ByVal s As Long, ByVal hwnd As
Long, ByVal wMsg As Long, ByVal
lEvent As Long) As Long
ولها أربع وسيطات :
: S المقبس الذي ستستقبل الدالة أحداثه, وهو طبعا LlistenSocket
Hwnd : مقبض النافذة التي سيتم إرسال أحداث المقبس إليها وهى frmServer ,ولا تنسي أننا
قمنا بعمل hook على هذه النافذة وسنستقبل الرسائل التي تصلها ونقدر
نحدد اى رسالة
خاصة بحدث للمقبس
عن طريق الوسيط الثالث wMsg
wMsg : وهى الرسالة التي سيستقبلها التصنيف الفرعى Winsockmain الموجود في الوحدة
النمطية mdlSubClass وهي قيمة ثابتةWM_SOCKET
وتجد تلك القيمة في تصريحات
الوحدة النمطية
mdlWinsock.وعندما يستقبل التصنيف الفرعي هذه الرسالة
WM_SOCKET
سيعرف أن هناك حدث للمقبس ويتم تحديد الحدث بجملة Select Case .
lEvent : من خلال هذا التابع يمكن تحديد الأحداث التي نريد مراقبتها وقد مررنا لها
الحدث
FD_ACCEPT وهو حدث
خاص باستشعار قدوم طلب اتصال من الطرف الأخر ..وبعدما
يحدث
الاتصال , سنعيد تمرير تلك الدالة مره أخرى لكن
لمراقبة هذه المرة أحداث أخرى للمقبس
مثل قدوم بيانات FD_READ أو حدث لإغلاق المقبس FD_CLOSE من الطرف الأخر ..
تعود الدالة بالقيمة صفر إذا نجحت في استقبال حدث
وإلا ستعود ب SOCKET_ERROR
___________________________________________________________________________
الآن ندخل في كتابة اكواد الوحدة النمطية mdlSubClass حتى تعرف ( هو فيه آية ؟)
لأن هذه الوحدة النمطية هي التي ستفسر
لك كل شيء عن أحداث المقبس .وكيفية تنظيم البيانات بعد إرسالها إليك من الطرف الأخر .
واكواد الوحدة النمطية بسيطة للغاية –
لكن مهم جدا أنك تكون فاهم باب
Hooking and SubClassing - :
Public
LlistenClient as long
Public
Function Winsockmain(ByVal hwnd
As Long, ByVal uMsg As
Long, _
ByVal wParam As Long, ByVal
lParam As Long) As Long
If uMsg = WM_SOCKET
Then
Dim ClientInternetAddress
As sockaddr_in, AddressLen As Long
Dim ReadBuffer(1024) As Byte
Dim Data As
String
On Error Resume Next
Select Case lParam
Case FD_ACCEPT
LlistenClient
= accept(wParam, ClientInternetAddress, ByVal AddressLen)
WSAAsyncSelect
LlistenClient, hwnd,
WM_SOCKET, FD_READ Or FD_CLOSE
frmServer.lblStatus.Caption
= "Connected"
If LlistenClient
= SOCKET_ERROR Then
frmServer.lblStatus.Caption
= "Disconnected"
End If
Case FD_READ
lRetVal
= recv(wParam, ReadBuffer(0), 1024, 0)
If lRetVal
> 0 Then
Data = Left(StrConv(ReadBuffer, vbUnicode), lRetVal)
If Left(Data,
5) = "Reply" Then
Data = Right(Data,
Len(Data) - 5)
frmServer.txtChat.Text
= frmServer.txtChat.Text & Data
End If
End If
Case FD_CLOSE
frmServer.lblStatus.Caption
= "The Remote Machine Disconnected"
End Select
End
If
Winsockmain = CallWindowProc(lpWinsock, hwnd, uMsg, wParam, lParam)
End Function
لقد قمنا بتحديد الرسالة التي ستحمل أحداث المقبس
عندما استخدمنا دالة WSASyncSelect() وحدننا لها القيمة الثابتة WM_SOCKET وكتبنا شرط إذا كانت الرسائل المرسلة للنافذة خاصة بالمقبس فيعمل تحدد حالات Select case
للأحداث الخاصة بالمقبس:
If uMsg = WM_SOCKET Then
Select Case lParam
'--------
'--------
End Select
End If
نلاحظ أن قيمة lParam تحمل نوع الرسالة (هل هيFD_ACCEPT ,FD_READ أم (FD_Close
أم قيمة wParam فتحمل مقبض Handle
للطرف الأتي من الرسالة, ولاحظ أننا - (بعد الموافقة على الاتصال( - سنستخدم القيمة المعادة من الدالة Accept() وهى LlistenClient لتمريرها
للدالة WSASyncSelect() مرة أخرى لاستقبال أحداث المقبس
الأخرى الخاصة بالجهاز الموجود عليه برنامج العميل Client الذي استقبلنا
منه الاتصال, وتمت مراقبة الحدث FD_Accept
بنجاح.
شرح بسيط لأحداث المقبس :
FD_ACCEPT : يتم إرسال هذا الحدث إلى مقبس برنامج الخادم Server عندما يطلب الطرف الأخر Client الاتصال
ببرنامجنا وسيتم الموافقة
على الاتصال القادم باستخدام دالة Accept()
وهذه
الدالة تشبه إلى حد كبير دالة bind().
Declare Function accept Lib "ws2_32.dll" (ByVal s As Long, ByRef addr As sockaddr_in ,
ByRef addrlen As Long) As Long
ولها ثلاث معاملات
:
S :
قيمة مقبض تمثل الطرف الأخر القادم منه الاتصال.
Addr : بنية من نوع sockaddr_in
يتم
ملؤها ببيانات العنوان للطرف الأخر.
Addrlen : حجم البنية .
FD_READ : يتم إرسال هذا الحدث إلى مقبس الخادم ..عندما يكون هناك بيانات قادمة من الطرف الأخر
ونستقبل تلك البيانات المرسلة بالدالةRecv() وهى كبيرة الشبة
بالدالة Send() .
Declare Function recv Lib "ws2_32.dll" (ByVal s As Long,ByRef buf As Any, _
ByVal buflen As Long,ByVal flags As Long) As Long
والمعاملات هي :
:
S قيمة مقبض الطرف الأخر .
buf : مخزن
معلوماتbuffer data .. ويخزن
به المعلومات المرسلة .
Buflen : حجم هذا المخزن بالبايت .
وقد قمنا بتعرف مصفوفة ReadBuffer(1024)
ليستقبل بايتات بحجم 1024 أي أن الخادم سيستقبل بايتات لفقط واحد كيلو
بايت في المرة الواحدة وإذا زادت عدد البايتات عن هذا الرقم فيجب زيادة ذلك الحجم
ليصبح ReadBuffer(8192) أو نقوم بالتأكد من وصول البيانات كاملة للطرف الأخر وذلك من خلال عمل
Loop لتقسيم البيانات وبحجم واحد كيلو
في المرة الواحدة.
فمثلاًً إذا
قمنا بتصميم مدير الملفات ((File Manager لأي غرض سواء لبرنامج هاك أو لأي برنامج خدمي وطلبنا رؤية أسماء مجلدات وملفات فإذا حاولت أن تستعرض محتوى مجلد وليكن مجلد System بحجم استقبال بيانات يصل
لواحد كيلو (1024) فلن تستطيع سوى استعراض 120
مجلد و
ملف على الأكثر مع أنك ستجد أن بمجلد System أكثر من 1000 ملف.وبالتالي يجب زيادة حجم المخزن Buffer
حتى يقوم باحتواء جميع البيانات المرسلة.
ولم يتبقى سوي تحويل تلك البايتات
التي استقبلناها داخل المصفوفة المعرفة كـ Byte وهي ReadBuffer(1024) إلى قيم Unicode حتى نستطيع قراءتها وذلك عن طريق استخدام دالة StrConv() (String Convert) هكذا
lRetVal
= recv(wParam, ReadBuffer(0), 1024, 0)
If lRetVal > 0
Then
Data = Left(StrConv(ReadBuffer,
vbUnicode), lRetVal)
'------
'------
End If
لاحظ أن الدالة recv()
عندما تستقبل بيانات فتعود
lRetVal
بعدد البايتات المستقبلة وهكذا
نستطيع أن نخزن القيم المحولة في المتغير Data باستخدام الدالةLeft() وطول السلسلة سيكون lRetVal وهو عدد البايتات المستقبلة ..
لاحظ أيضا أن الرسالة
القادمة من الطرف الأخر ستكون هكذا
data=ReplayHi back
وهنا سنضطر إلى قطع الكلمة replay بدوال التعامل
مع النصوص :
If Left(Data,
5) = "Reply" Then
data =
Right(Data, Len(Data) – 5)
If Left(Data,
5) = "Reply" Then
Data = Right(Data, Len(Data) - 5
frmServer.txtChat.Text
= frmServer.txtChat.Text & VbCrlf
& Data
End If
الآن أصبح المتغير data يحمل القيمة Hi back ومنها نظهر تلك القيمة في txtchat
FD_CLOSE : يستقبل المقبس
هذا الحدث إذا تم إغلاق الاتصال من الطرف الأخر ..ويمكننا في
هذه الحالة إظهار رسالة تبين أنه لم يعد هناك
اتصال مع الطرف الأخر .
طبعا في
النهاية لا تنسى أن تمرر رسائل المقبس بشكل طبيعيي إلى النافذة الرئيسية frmserver التي قمنا بعمل
Hook عليها
لأننا إذا لم
نمرر تلك الرسائل كما عرفنا من الجزء الخاص بـ Hooks and Subclassing فلن يستقبل الفورمFrmServer أي حدث للمقبض سواء كان بيانات الشات (Chat) أو حالة الاتصال وهكذا .
Winsockmain = CallWindowProc(lpWinsock, hwnd,
uMsg, wParam, lParam)
وهكذا نكون قد انتهينا من برمجة الخادم .ويبقى أن
نبرمج ألClient ..
=====================
Winsock
API Tutorial Part 4
Written
By
Mohamed
Fayed
=====================
More Info Visit The source of Network
Programming
www.vbip.com